import hmac
import base64
import random
import time
import warnings
try:
    from hashlib import sha1
except ImportError:
    import sha as sha1
from paste.request import get_cookies

def make_time(value):
    return time.strftime('%Y%m%d%H%M', time.gmtime(value))


_signature_size = len(hmac.new('x', 'x', sha1).digest())
_header_size = (_signature_size + len(make_time(time.time())))
_encode = [('\\', '\\x5c'),
 ('"', '\\x22'),
 ('=', '\\x3d'),
 (';', '\\x3b')]
_decode = [ (v, k) for (k, v,) in _encode ]
_decode.reverse()

def encode(s, sublist = _encode):
    return reduce(lambda a, .1:     (b, c,) = .1
a.replace(b, c), sublist, str(s))


decode = lambda s: encode(s, _decode)

class CookieTooLarge(RuntimeError):

    def __init__(self, content, cookie):
        RuntimeError.__init__('Signed cookie exceeds maximum size of 4096')
        self.content = content
        self.cookie = cookie



_all_chars = ''.join([ chr(x) for x in range(0, 255) ])

def new_secret():
    return ''.join(random.sample(_all_chars, 64))



class AuthCookieSigner(object):

    def __init__(self, secret = None, timeout = None, maxlen = None):
        self.timeout = (timeout or 30)
        if isinstance(timeout, basestring):
            raise ValueError(('Timeout must be a number (minutes), not a string (%r)' % timeout))
        self.maxlen = (maxlen or 4096)
        self.secret = (secret or new_secret())



    def sign(self, content):
        cookie = base64.encodestring(((hmac.new(self.secret, content, sha1).digest() + make_time((time.time() + (60 * self.timeout)))) + content))
        cookie = cookie.replace('/', '_').replace('=', '~')
        cookie = cookie.replace('\n', '').replace('\r', '')
        if (len(cookie) > self.maxlen):
            raise CookieTooLarge(content, cookie)
        return cookie



    def auth(self, cookie):
        decode = base64.decodestring(cookie.replace('_', '/').replace('~', '='))
        signature = decode[:_signature_size]
        expires = decode[_signature_size:_header_size]
        content = decode[_header_size:]
        if ((signature == hmac.new(self.secret, content, sha1).digest()) and (int(expires) > int(make_time(time.time())))):
            return content




class AuthCookieEnviron(list):

    def __init__(self, handler, scanlist):
        list.__init__(self, scanlist)
        self.handler = handler



    def append(self, value):
        if (value in self):
            return 
        list.append(self, str(value))




class AuthCookieHandler(object):
    environ_name = 'paste.auth.cookie'
    cookie_name = 'PASTE_AUTH_COOKIE'
    signer_class = AuthCookieSigner
    environ_class = AuthCookieEnviron

    def __init__(self, application, cookie_name = None, scanlist = None, signer = None, secret = None, timeout = None, maxlen = None):
        if not signer:
            signer = self.signer_class(secret, timeout, maxlen)
        self.signer = signer
        self.scanlist = (scanlist or ('REMOTE_USER', 'REMOTE_SESSION'))
        self.application = application
        self.cookie_name = (cookie_name or self.cookie_name)



    def __call__(self, environ, start_response):
        if (self.environ_name in environ):
            raise AssertionError('AuthCookie already installed!')
        scanlist = self.environ_class(self, self.scanlist)
        jar = get_cookies(environ)
        if jar.has_key(self.cookie_name):
            content = self.signer.auth(jar[self.cookie_name].value)
            if content:
                for pair in content.split(';'):
                    (k, v,) = pair.split('=')
                    k = decode(k)
                    if (k not in scanlist):
                        scanlist.append(k)
                    if (k in environ):
                        continue
                    environ[k] = decode(v)
                    if ('REMOTE_USER' == k):
                        environ['AUTH_TYPE'] = 'cookie'

        environ[self.environ_name] = scanlist
        if ('paste.httpexceptions' in environ):
            warnings.warn('Since paste.httpexceptions is hooked in your processing chain before paste.auth.cookie, if an HTTPRedirection is raised, the cookies this module sets will not be included in your response.\n')

        def response_hook(status, response_headers, exc_info = None):
            scanlist = environ.get(self.environ_name)
            content = []
            for k in scanlist:
                v = environ.get(k)
                if (v is not None):
                    if (type(v) is not str):
                        raise ValueError(('The value of the environmental variable %r is not a str (only str is allowed; got %r)' % (k, v)))
                    content.append(('%s=%s' % (encode(k), encode(v))))

            if content:
                content = ';'.join(content)
                content = self.signer.sign(content)
                cookie = ('%s=%s; Path=/;' % (self.cookie_name, content))
                if ('https' == environ['wsgi.url_scheme']):
                    cookie += ' secure;'
                response_headers.append(('Set-Cookie', cookie))
            return start_response(status, response_headers, exc_info)


        return self.application(environ, response_hook)



middleware = AuthCookieHandler

def make_auth_cookie(app, global_conf, cookie_name = 'PASTE_AUTH_COOKIE', scanlist = ('REMOTE_USER', 'REMOTE_SESSION'), secret = None, timeout = 30, maxlen = 4096):
    if isinstance(scanlist, basestring):
        scanlist = scanlist.split()
    if ((secret is None) and global_conf.get('secret')):
        secret = global_conf['secret']
    try:
        timeout = int(timeout)
    except ValueError:
        raise ValueError(('Bad value for timeout (must be int): %r' % timeout))
    try:
        maxlen = int(maxlen)
    except ValueError:
        raise ValueError(('Bad value for maxlen (must be int): %r' % maxlen))
    return AuthCookieHandler(app, cookie_name=cookie_name, scanlist=scanlist, secret=secret, timeout=timeout, maxlen=maxlen)


__all__ = ['AuthCookieHandler',
 'AuthCookieSigner',
 'AuthCookieEnviron']
if ('__main__' == __name__):
    import doctest
    doctest.testmod(optionflags=doctest.ELLIPSIS)

